from typing import List, Sequence
import numpy as np
from scipy.signal import butter, filtfilt


def filter_signal(x: Sequence[float], fs: float, cutoff: float = 8.0, order: int = 4) -> List[float]:
    """Butterworth low-pass; default cutoff 8 Hz suitable for upper-limb.
    Falls back to raw if signal too short.
    """
    x_arr = np.asarray(x, dtype=float)
    if len(x_arr) < max(10, order*3):
        return x_arr.tolist()
    nyq = 0.5 * fs
    w = cutoff / nyq
    b, a = butter(order, w, btype='low')
    try:
        y = filtfilt(b, a, x_arr)
    except Exception:
        # Fallback to simple forward-backward IIR if filtfilt fails
        y = np.convolve(x_arr, np.ones(5)/5, mode='same')
    return y.tolist()


def segment_window(multivar: Sequence[Sequence[float]], window_size: int, step: int) -> List[np.ndarray]:
    """Segment multivariate time series into overlapping windows.
    multivar: list of samples, each sample is a list of channels.
    Returns list of arrays with shape (window_size, num_channels).
    """
    X = np.asarray(multivar, dtype=float)
    n = len(X)
    num_channels = X.shape[1] if X.ndim == 2 else 1
    windows = []
    for start in range(0, max(1, n - window_size + 1), step):
        end = start + window_size
        if end <= n:
            windows.append(X[start:end].reshape(window_size, num_channels))
    if not windows and n > 0:
        # If not enough length, take entire series as one window
        windows.append(X.reshape(n, num_channels))
    return windows


def normalize(window: np.ndarray) -> np.ndarray:
    """Z-score per channel; safe for constant channels."""
    mean = window.mean(axis=0)
    std = window.std(axis=0)
    std_safe = np.where(std < 1e-8, 1.0, std)
    return (window - mean) / std_safe